diff --git a/src/AdventOfCode/Day23.cs b/src/AdventOfCode/Day23.cs
new file mode 100644
index 0000000..540eaa0
--- /dev/null
+++ b/src/AdventOfCode/Day23.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.Linq;
+using AdventOfCode.Utilities;
+
+namespace AdventOfCode
+{
+ ///
+ /// Solver for Day 23
+ ///
+ public class Day23
+ {
+ public int Part1(string[] input)
+ {
+ SortedDictionary> graph = BuildGraph(input);
+
+ int total = 0;
+
+ /* Look for triangles of interconnected nodes, i.e.
+
+ node
+ / \
+ / \
+ left -- right
+ */
+ foreach (string node in graph.Keys)
+ {
+ foreach (string left in graph[node].Where(l => string.CompareOrdinal(node, l) < 0))
+ {
+ foreach (string right in graph[left].Where(r => string.CompareOrdinal(left, r) < 0))
+ {
+ if (!graph[node].Contains(right))
+ {
+ continue;
+ }
+
+ // found a triangle
+ if (node.StartsWith('t') || left.StartsWith('t') || right.StartsWith('t'))
+ {
+ total++;
+ }
+ }
+ }
+ }
+
+ return total;
+ }
+
+ public string Part2(string[] input)
+ {
+ SortedDictionary> graph = BuildGraph(input);
+
+ List biggest = [];
+ HashSet visited = [];
+
+ foreach (string node in graph.Keys)
+ {
+ if (!visited.Add(node))
+ {
+ // already part of another clique
+ continue;
+ }
+
+ List clique = [node];
+
+ foreach (string candidate in graph.Keys)
+ {
+ if (visited.Contains(candidate))
+ {
+ // already part of another clique
+ continue;
+ }
+
+ // if it's connected to every current member of the clique then it's allowed in
+ if (clique.All(member => graph[member].Contains(candidate)))
+ {
+ clique.Add(candidate);
+ visited.Add(candidate);
+ }
+ }
+
+ if (clique.Count > biggest.Count)
+ {
+ biggest = clique;
+ }
+ }
+
+ return string.Join(',', biggest.Order());
+ }
+
+ ///
+ /// Build the graph of every node to all the reachable nodes from that point
+ ///
+ /// Input edge descriptions
+ /// Graph
+ private static SortedDictionary> BuildGraph(string[] input)
+ {
+ SortedDictionary> graph = new();
+
+ foreach (string line in input)
+ {
+ string[] elements = line.Split('-');
+
+ graph.GetOrCreate(elements[0], () => new List()).Add(elements[1]);
+ graph.GetOrCreate(elements[1], () => new List()).Add(elements[0]);
+ }
+
+ return graph;
+ }
+ }
+}
diff --git a/src/AdventOfCode/inputs/day23.txt b/src/AdventOfCode/inputs/day23.txt
new file mode 100644
index 0000000..192657c
Binary files /dev/null and b/src/AdventOfCode/inputs/day23.txt differ
diff --git a/tests/AdventOfCode.Tests/Day23Tests.cs b/tests/AdventOfCode.Tests/Day23Tests.cs
new file mode 100644
index 0000000..41c7eb3
--- /dev/null
+++ b/tests/AdventOfCode.Tests/Day23Tests.cs
@@ -0,0 +1,105 @@
+using System.IO;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace AdventOfCode.Tests
+{
+ public class Day23Tests
+ {
+ private readonly ITestOutputHelper output;
+ private readonly Day23 solver;
+
+ public Day23Tests(ITestOutputHelper output)
+ {
+ this.output = output;
+ this.solver = new Day23();
+ }
+
+ private static string[] GetRealInput()
+ {
+ string[] input = File.ReadAllLines("inputs/day23.txt");
+ return input;
+ }
+
+ private static string[] GetSampleInput()
+ {
+ return new string[]
+ {
+ "kh-tc",
+ "qp-kh",
+ "de-cg",
+ "ka-co",
+ "yn-aq",
+ "qp-ub",
+ "cg-tb",
+ "vc-aq",
+ "tb-ka",
+ "wh-tc",
+ "yn-cg",
+ "kh-ub",
+ "ta-co",
+ "de-co",
+ "tc-td",
+ "tb-wq",
+ "wh-td",
+ "ta-ka",
+ "td-qp",
+ "aq-cg",
+ "wq-ub",
+ "ub-vc",
+ "de-ta",
+ "wq-aq",
+ "wq-vc",
+ "wh-yn",
+ "ka-de",
+ "kh-ta",
+ "co-tc",
+ "wh-qp",
+ "tb-vc",
+ "td-yn",
+ };
+ }
+
+ [Fact]
+ public void Part1_SampleInput_ProducesCorrectResponse()
+ {
+ var expected = 7;
+
+ var result = solver.Part1(GetSampleInput());
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Part1_RealInput_ProducesCorrectResponse()
+ {
+ var expected = 1599;
+
+ var result = solver.Part1(GetRealInput());
+ output.WriteLine($"Day 23 - Part 1 - {result}");
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Part2_SampleInput_ProducesCorrectResponse()
+ {
+ var expected = "co,de,ka,ta";
+
+ var result = solver.Part2(GetSampleInput());
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void Part2_RealInput_ProducesCorrectResponse()
+ {
+ var expected = "av,ax,dg,di,dw,fa,ge,kh,ki,ot,qw,vz,yw";
+
+ var result = solver.Part2(GetRealInput());
+ output.WriteLine($"Day 23 - Part 2 - {result}");
+
+ Assert.Equal(expected, result);
+ }
+ }
+}