Determining orthogonal polygons in a 3D integer lattice
Type Hints
+1 for using type hints.  But they could be a little more informative.
def polygonise(pts: list) -&gt; list:
Ok.  The function takes a list of pts, but what are pts?  It returns a list, but a list of what?
from typing import Sequence

Point = Sequence[int]
Polygon = list[Point]

def polygonise(points: list[Point]) -&gt; list[Polygon]:
    ...

Now we're giving the reader a lot of information.  A point is a sequence of integers.  Could be a tuple, could be a list.  Not important.  Instead of pts, we're a bit more explicit and say points.  Finally, the function returns a specific kind of list: a Polygon list.
You can help the reader out even more by including a nice &quot;&quot;&quot;docstring&quot;&quot;&quot; for the function.
Axes
axes = {i:set(c) for i,c in enumerate(zip(*pts)) if len(set(c))!=1}
I really, really hate this statement.  It takes the list of points, and explodes each point as an argument for zip.  Then, it takes the first coordinate of each of those points, and returns it as a tuple.  enumerate adds an index to this, to keep track of which coordinate axis we are processing.  Then, we assign those to things to i, c ... really opaque variable names.  axis, coordinates might be clearer.  Then we turn the coordinates into a set, determine the len of the set, and if that is not 1, we turn the coordinates into a set again, for construction the dictionary.
Yikes!  That's a lot to unpack.
Then we take the axes, and assign it into to variables a and b, hoping there are exactly 2 axis.
Perhaps we should add some error checking.
    num_coord = len(points[0])
    if any(len(pt) != num_coord for pt in points):
        raise ValueError(&quot;All points should have the same number of coordinates&quot;)

    axes = ...

    if len(axes) != 2:
        raise ValueError(&quot;All coordinates must be in an orthogonal 2D plane&quot;)

    a, b = axes.keys()

That gives me a lot more confidence in the code.
I still don't like the axes = ... statement.  I would re-write it, and maybe use the walrus operator (:=) to avoid creating the set twice, but I'm about to significantly change it.
Useless code
edges[tuple((axis,v1))] = other, v2
Here, (axis, v1) is creating a tuple, to pass to the tuple(...) function.  That seems ... odd.  You could simplify it to:
edges[axis, v1] = other, v2
Alternate implementation
Axes
Instead of a dictionary, let's just extract a list of axis which have varying coordinate data.  A simple comparison of each coordinate of all points with the coordinate of the first point, for each coordinate, will tell us if that axis is important or not.
    axes = [axis for axis, first in enumerate(points[0])
            if any(pt[axis] != first for pt in points)]

    if len(axes) != 2:
        raise ValueError(&quot;All coordinates must be in an orthogonal 2D plane&quot;)

    a, b = axes

This is a much simpler concept.  No sets are being created (twice).  No dictionaries.
Sorted Edges
The pts_by_axis is very complex, a dictionary of dictionaries of arrays.
Let's rework this, by simply sorting the points list.  We'll sort it two ways, first by the (a, b) coordinate, then by the (b, a) coordinate.  We'll use a helper function to generate the sort keys:
def point_key(first: int, second: int):
    def key(pt):
        return pt[first], pt[second]
    return key

Now we can say sorted(points, key=point_key(a, b)), and we'll have the points sorted in one direction.  sorted(points, key=point_key(b, a)) will sort it in the other direction.
Since the points always come in pairs, to form edges, let's add another helper function, to extract pairs of points from the sorted array.
def in_pairs(iterable):
    it = iter(iterable)
    return zip(it, it)

With these, we can build our sorted edge structure.  But instead of keying the edges dictionary with a tuple to encoding the axis of the edge, let's just use two dictionaries:
    a_edge = {}
    for v1, v2 in in_pairs(sorted(points, key=point_key(a, b))):
        a_edge[v1] = v2
        a_edge[v2] = v1

    b_edge = {}
    for v1, v2 in in_pairs(sorted(points, key=point_key(b, a))):
        b_edge[v1] = v2
        b_edge[v2] = v1

The loop at the end does some check to close a polygon and start the next one.  Really, you have two loops.  Loop while there are still edges.  Inside that loop, loop until you've made a polygon.  With that structure, the code reads much better:
    result = []
    while a_edge:
        v1 = next(iter(a_edge.keys()))
        poly = []

        while v1 in a_edge:
            v2 = a_edge.pop(v1)
            v3 = b_edge.pop(v2)
            a_edge.pop(v2)
            b_edge.pop(v1)
            poly.extend((v1, v2))
            v1 = v3

        result.append(poly)
        
    return result

Note that I use poly = [] instead of poly = [v1], which means no slice is required at the end to remove the extra point.
Also, I'm popping v2 out of the a_edge dictionary, not a edge, since what is popped out is just a vertex.  I find that much clearer.