diff --git a/src/meshTools/Make/files b/src/meshTools/Make/files
index 6218645011..4a571b5fc2 100644
--- a/src/meshTools/Make/files
+++ b/src/meshTools/Make/files
@@ -352,4 +352,6 @@ regionModel/regionProperties/regionProperties.C
tetOverlapVolume/tetOverlapVolume.C
+triangulatedPatch/triangulatedPatch.C
+
LIB = $(FOAM_LIBBIN)/libmeshTools
diff --git a/src/meshTools/triangulatedPatch/triangulatedPatch.C b/src/meshTools/triangulatedPatch/triangulatedPatch.C
new file mode 100644
index 0000000000..513514cfbf
--- /dev/null
+++ b/src/meshTools/triangulatedPatch/triangulatedPatch.C
@@ -0,0 +1,224 @@
+/*---------------------------------------------------------------------------*\
+ ========= |
+ \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
+ \\ / O peration |
+ \\ / A nd | www.openfoam.com
+ \\/ M anipulation |
+-------------------------------------------------------------------------------
+ Copyright (C) 2023 OpenCFD Ltd.
+-------------------------------------------------------------------------------
+License
+ This file is part of OpenFOAM.
+
+ OpenFOAM is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with OpenFOAM. If not, see .
+
+\*---------------------------------------------------------------------------*/
+
+#include "triangulatedPatch.H"
+#include "triPointRef.H"
+
+// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
+
+bool Foam::triangulatedPatch::randomPoint
+(
+ Random& rnd,
+ const scalar c,
+ point& result,
+ label& facei,
+ label& celli
+) const
+{
+ result = point::min;
+ facei = -1;
+ celli = -1;
+
+ if (triWght_.empty() || c < triWght_.front() || c > triWght_.back())
+ {
+ return false;
+ }
+
+ // Find corresponding decomposed face triangle
+ // Note: triWght_ is sized nTri+1 (zero added at start)
+ label trii = 0;
+ for (label i = 0; i < triWght_.size() - 1; ++i)
+ {
+ if (c > triWght_[i] && c <= triWght_[i+1])
+ {
+ trii = i;
+ break;
+ }
+ }
+
+ // Find random point in triangle
+ const pointField& points = patch_.points();
+ const face& tf = triFace_[trii];
+ const triPointRef tri(points[tf[0]], points[tf[1]], points[tf[2]]);
+
+ result = tri.randomPoint(rnd);
+ facei = triToFace_[trii];
+ celli = patch_.faceCells()[facei];
+
+ if (perturbTol_ > 0)
+ {
+ const polyMesh& mesh = patch_.boundaryMesh().mesh();
+ const point& cc = mesh.cellCentres()[celli];
+
+ // Normal points out of domain => subtract correction
+ const vector& n = patch_.faceNormals()[facei];
+ result -= perturbTol_*n*mag(n & (cc - result));
+
+ // Reset facei - point no longer resides on the face
+ facei = -1;
+ }
+
+ return true;
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
+
+Foam::triangulatedPatch::triangulatedPatch
+(
+ const polyPatch& patch,
+ const scalar perturbTol
+)
+:
+ patch_(patch),
+ perturbTol_(perturbTol),
+ triFace_(),
+ triToFace_(),
+ triWght_()
+{
+ update();
+}
+
+
+Foam::triangulatedPatch::triangulatedPatch
+(
+ const polyMesh& mesh,
+ const word& patchName,
+ const scalar perturbTol
+)
+:
+ triangulatedPatch(mesh.boundaryMesh()[patchName], perturbTol)
+{}
+
+
+// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
+
+void Foam::triangulatedPatch::update()
+{
+ const pointField& points = patch_.points();
+
+ // Triangulate the patch faces and create addressing
+ DynamicList