diff --git a/src/polymesh/algorithms.hh b/src/polymesh/algorithms.hh
index 759b1c26072a9ea192c0f5d6219ddb732f7e41a2..813da2947e2bbb741414366889f77f89c1fba63e 100644
--- a/src/polymesh/algorithms.hh
+++ b/src/polymesh/algorithms.hh
@@ -21,6 +21,7 @@
 #include "algorithms/deduplicate.hh"
 #include "algorithms/delaunay.hh"
 #include "algorithms/edge_split.hh"
+#include "algorithms/fill_hole.hh"
 #include "algorithms/interpolation.hh"
 #include "algorithms/iteration.hh"
 #include "algorithms/normalize.hh"
diff --git a/src/polymesh/algorithms/fill_hole.hh b/src/polymesh/algorithms/fill_hole.hh
new file mode 100644
index 0000000000000000000000000000000000000000..85d34d0d333d62bb61785fe50d206668708f15b1
--- /dev/null
+++ b/src/polymesh/algorithms/fill_hole.hh
@@ -0,0 +1,116 @@
+#pragma once
+
+#include <vector>
+
+#include <polymesh/Mesh.hh>
+#include <polymesh/fields.hh>
+
+namespace polymesh
+{
+/// Fills a hole given by the boundary halfdedge "boundary_start" using dynamic programming to compute the area minimizing triangulation
+template <class Pos3>
+void fill_hole(Mesh& m, vertex_attribute<Pos3> const& position, halfedge_handle boundary_start)
+{
+    POLYMESH_ASSERT(boundary_start.is_boundary());
+
+    // This is a dynamic programming approach that fills a two-dimensional table of weights.
+    // The boundary is indexed by numbers ranging from 0 to n.
+    // The entry weights[x,y] (accessed via weights[index_of(x,y)]) gives the minimal weight to triangulate the boudary from vertex x to vertex y.
+    // Since only entries that span at least three vertices can span any triangles, only these need to be stored.
+    // The other entries are implicitly zero.
+    // This means, x ranges from 0 to n-1 and y ranges from 1 to n.
+    // In this implementation, the weight is equal to the triangle area.
+    // To extract the correct triangulation after the table of weights has been computed, a chosen triangle is also associated with each entry in the table.
+
+    std::vector<pm::vertex_index> boundary;
+    { // fill boundary
+        auto current = boundary_start;
+        do
+        {
+            boundary.push_back(current.vertex_to());
+            current = current.next();
+        } while (current != boundary_start);
+    }
+
+    auto const n = int(boundary.size()) - 1;
+    // only entries in the lower left half of the table can have non-zero entries.
+    auto const table_size = (n * (n - 1)) / 2;
+
+    auto weights = std::vector<float>();
+    auto chosen_triangle = std::vector<int>();
+    weights.resize(table_size);
+    chosen_triangle.resize(table_size);
+
+    auto const index_of = [&](int x, int y) -> int {
+        POLYMESH_ASSERT(0 <= x && x <= n - 1);
+        POLYMESH_ASSERT(1 <= y && y <= n);
+        POLYMESH_ASSERT(x < y);
+        return x + ((y - 1) * (y - 2)) / 2;
+    };
+
+    auto const weight_at = [&](int x, int y) -> float {
+        if (x + 1 == y) // the diagonal is 0, no need to store it
+            return 0.0f;
+        return weights[index_of(x, y)];
+    };
+
+    auto const triangle_at = [&](int x, int y) -> int {
+        if (x + 2 == y) // only one unique triangle can be chosen here
+            return x + 1;
+        return chosen_triangle[index_of(x, y)];
+    };
+
+    auto const triangle_area = [&](int x, int y, int z) {
+        auto const p0 = position[boundary[x]];
+        auto const p1 = position[boundary[y]];
+        auto const p2 = position[boundary[z]];
+        return field3<Pos3>::length(field3<Pos3>::cross(p0 - p1, p0 - p2)) * field3<Pos3>::scalar(0.5f);
+    };
+
+    // initialize with triangle sizes
+    for (auto i = 0; i < n - 2; ++i)
+        weights[index_of(i, i + 2)] = triangle_area(i, i + 1, i + 2);
+
+    // fill the table using dynamic programming
+    for (auto d = 3; d <= n; ++d)
+    {
+        for (auto i = 0; i < n - d + 1; ++i)
+        {
+            auto const x = i;
+            auto const y = d + i;
+
+            // find the optimal triangulation for the boundary from vertex x to vertex y.
+
+            auto min_weight = weight_at(x, x + 1) + triangle_area(x, x + 1, y) + weight_at(x + 1, y);
+            int t = x + 1;
+            for (auto k = 2; x + k < y; ++k)
+            {
+                auto const w = weight_at(x, x + k) + triangle_area(x, x + k, y) + weight_at(x + k, y);
+                if (w < min_weight)
+                {
+                    min_weight = w;
+                    t = x + k;
+                }
+            }
+
+            weights[index_of(x, y)] = min_weight;
+            chosen_triangle[index_of(x, y)] = t;
+        }
+    }
+
+    // backtrack the chosen triangles
+    std::vector<std::pair<int, int>> stack;
+    stack.push_back({0, n});
+    while (!stack.empty())
+    {
+        auto const [a, c] = stack.back();
+        stack.pop_back();
+        auto const b = triangle_at(a, c);
+        m.faces().add(boundary[a].of(m), boundary[b].of(m), boundary[c].of(m));
+        if (a + 1 < b)
+            stack.push_back({a, b});
+        if (b + 1 < c)
+            stack.push_back({b, c});
+    }
+}
+}