How to create a button on the n-panel?
I suggest start here: How to create a custom UI?

To extend the Properties Area of the 3D View you can either register a custom panel to add UI elements onto or you can append elements to existing panels.



1. Panel

Blender comes with a few UI templates (Text Editor > Templates > UI ...) . I'd suggest start with UI Panel Simple ui_panel_simple.py and use it as base to work on. You just have to edit its default variables in order to display the HelloWorldPanel as part of the properties panel:


Set bl_space_type to 'VIEW_3D' to make it part of the 3D View
Set bl_context to eg. 'mesh_edit' or 'objectmode' to either match Edit or Object Mode


class HelloWorldPanel(bpy.types.Panel):
    """Creates a Sub-Panel in the Property Area of the 3D View"""
    bl_label = "My Tool"  
    bl_space_type = "VIEW_3D"  
    bl_region_type = "UI"
    bl_category = "My Tools"
    bl_context = "mesh_edit" # "objectmode"


The HelloWorldPanel comes with a predefined draw method where you can edit the layout as well as adding UI elements to the panel. More interestingly: You can easily add existing operators like bpy.ops.mesh.primitive_cube_add() to the panel by calling the operator() method on the layout element like row.operator("mesh.primitive_cube_add") without the namespace (bpy.ops*).

row = layout.row()
# bpy.ops.mesh.primitive_cube_add()
row.operator("mesh.primitive_cube_add")

row = layout.row()
# bpy.ops.object.origin_set()
row.operator("object.origin_set").type="GEOMETRY_ORIGIN"


To pass multiple arguments read: How to pass multiple operator properties via UI layout?



2. Operator

If you want a custom function assigned to a button, you'll have to create a new operator. Blender comes with operator_simple.py template which is easy to understand. In most cases it's enough adding your code to the execute method, which will be called when the button is pressed. 

def execute(self, context):
    my_variable = "Hello World"
    print (my_variable) # Prints 'Hello World' to the Console
    self.report({'INFO'}, my_variable) # Reports 'Hello World' to the Info Area
    return {'FINISHED'} # Return the execution is finished


If you want to call another operator as part of the operation like bpy.ops.mesh.remove_doubles() which only works on meshes, it's a good idea to limit the scope to mesh objects using the provided poll method, even if the operator is only accessible in Edit Mode because otherwise the operator is still callable using the Search Menu (F3):

@classmethod
def poll(cls, context):
    obj = context.active_object
    return (obj is not None and obj.type == 'MESH')

def execute(self, context):
    # Your code here
    bpy.ops.mesh.remove_doubles(threshold=0.02)
    # ...
    return {'FINISHED'}


Also read: What do operator methods do? (poll, invoke, execute, draw &amp; modal)

If you want to display the vertex count after mesh.remove_doubles, you can store the vertex count before calling the operator and then report the actual result:

def execute(self, context):
    obj = context.object
    verts_before = len(obj.data.vertices)
    bpy.ops.mesh.remove_doubles(threshold=0.02)
    verts_after = len(obj.data.vertices)
    result = verts_before - verts_after
    self.report({'INFO'}, "Verts removed: {}".format(result))
    return {'FINISHED'}




3. Registration

Register your classes properly, add your operator(s), labels, and properties to the panel:

import bpy

class MY_OT_custom(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return (obj is not None and obj.type == 'MESH')

    def execute(self, context):
        # Your code here 
        # ...
        bpy.ops.mesh.remove_doubles(threshold=0.02)   
        return {'FINISHED'}


class MY_PT_custom(bpy.types.Panel):
    """Creates a Sub-Panel in the Property Area of the 3D View"""
    bl_label = "My Tool"  
    bl_space_type = "VIEW_3D"  
    bl_region_type = "UI"
    bl_category = "My Tools"
    bl_context = "mesh_edit"

    def draw(self, context):
        obj = context.object

        layout = self.layout

        row = layout.row()
        row.label(text="Active object is: {}".format(obj.name))

        row = layout.row()
        row.prop(obj, "name")

        row = layout.row()
        row.label(text="Vertex Count: {}".format(len(obj.data.vertices)))

        row = layout.row()
        row.operator(MY_OT_custom.bl_idname)



def register():
    bpy.utils.register_class(MY_PT_custom)
    bpy.utils.register_class(MY_OT_custom)


def unregister():
    bpy.utils.unregister_class(MY_OT_custom)
    bpy.utils.unregister_class(MY_PT_custom)


if __name__ == "__main__":
    register()




Appendix

TLDR; If you want to 'remove doubles' (merge by distance) for multiple meshes in Object Mode for whatever reason, use the bmesh.ops.remove_doubles() operator as suggested by @batFINGER, it's 3 times faster.

bpy.ops.mesh.remove_doubles()

As of 2.8x we can enter Edit Mode for all selected objects simultaneously, which avoids looping over all the objects as well as switching to Edit Mode and Object Mode each iteration so your code can be simplified as follows:

def execute(self, context):
    # get all mesh objects in selection 
    meshes = set(o for o in context.selected_objects if o.type == 'MESH')
    # store the vert count on all objects
    verts_before = sum([len(o.data.vertices) for o in meshes])

    # switch to edit mode
    bpy.ops.object.mode_set(mode='EDIT')
    # select all vertices
    bpy.ops.mesh.select_all(action='SELECT')
    # remove the doubles
    bpy.ops.mesh.remove_doubles(threshold=0.01)
    # switch to object mode
    bpy.ops.object.mode_set(mode='OBJECT')

    # store the vert count again
    verts_after = sum([len(o.data.vertices) for o in meshes])
    self.report({'INFO'}, "Verts removed: {}".format(verts_before-verts_after))

    return {'FINISHED'}


Although this saves a lot of time, it still takes 0.0814 seconds for 100 cubes with a vertex count of 8080.

bmesh.ops.remove_doubles()

    def execute(self, context):
        # get all mesh objects in selection 
        meshes = set(o.data for o in context.selected_objects if o.type == 'MESH')
        # store the vert count on all objects
        verts_before = sum([len(o.vertices) for o in meshes])

        # Remove doubles using bmesh based on:
        # https://blender.stackexchange.com/a/68133/31447
        bm = bmesh.new()
        for m in meshes:
            bm.from_mesh(m)
            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.merge_dist)
            bm.to_mesh(m)
            m.update()
            bm.clear()
        bm.free()

        # store the vert count again
        verts_after = sum([len(o.vertices) for o in meshes])
        self.report({'INFO'}, "Verts removed: {}".format(verts_before-verts_after))
        return {'FINISHED'}


Bmesh method takes 0.0290 seconds for 100 cubes with a vertex count of 8080.



Final Panel &amp; Operator

Panel in Object Mode and a proper implementation of the operator (Undo, Merge Distance etc.) in order to remove possible doubles for all objects in selection:

import bpy
import bmesh

class MY_OT_rem_doubles_bmesh(bpy.types.Operator):
    """Remove Doubles on Objects in Selection"""
    bl_idname = "object.remove_doubles_bmesh"
    bl_label = "Remove Doubles (bmesh)"
    bl_options = {'REGISTER', 'UNDO'}

    merge_dist: bpy.props.FloatProperty(
        name="Merge Distance",
        description="Merge Distance",
        min=0.0,
        step=0.1,
        default=0.02
    )

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return (obj is not None and obj.type == 'MESH')

    def execute(self, context):
        meshes = set(o.data for o in context.selected_objects if o.type == 'MESH')
        verts_before = sum(len(o.vertices) for o in meshes)

        # Remove doubles based on:
        # https://blender.stackexchange.com/a/68133/31447
        bm = bmesh.new()
        for m in meshes:
            bm.from_mesh(m)
            bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.merge_dist)
            bm.to_mesh(m)
            m.update()
            bm.clear()
        bm.free()

        verts_after = sum([len(o.vertices) for o in meshes])
        self.report({'INFO'}, "Verts removed: {}".format(verts_before-verts_after))

        return {'FINISHED'}



class MY_PT_custom(bpy.types.Panel):
    """Creates a Sub-Panel in the Property Area of the 3D View"""
    bl_label = "My Tools"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "Tool"
    bl_context = "objectmode"

    def draw(self, context):
        sel_objs = context.selected_objects
        sel_vert_count = sum(len(o.data.vertices) for o in sel_objs if o.type == 'MESH')

        layout = self.layout

        row = layout.row()
        row.operator(MY_OT_rem_doubles_bmesh.bl_idname)
        layout.separator()

        row = layout.row()
        row.label(text="{} Objects in Selection".format(len(sel_objs)))

        row = layout.row()
        row.label(text="Vertex Count: {}".format(sel_vert_count))



def register():
    bpy.utils.register_class(MY_OT_rem_doubles_bmesh)
    bpy.utils.register_class(MY_PT_custom)


def unregister():
    bpy.utils.unregister_class(MY_OT_rem_doubles_bmesh)
    bpy.utils.unregister_class(MY_PT_custom)


if __name__ == "__main__":
    register()