diff --git a/CHANGELOG.md b/CHANGELOG.md
index e39da0a..f6ac18b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,6 +35,8 @@ Bug fixes:
 - Fix `to_parquet`/`to_feather` to use correct WKB flavor for 3D geometries (#2654)
 - Fix `read_file` to avoid reading all file bytes prior to calling Fiona or
   Pyogrio if provided a URL as input (#2796)
+- Fix `copy()` downcasting GeoDataFrames without an active geometry column to a 
+  DataFrame (#2775)
 
 Notes on (optional) dependencies:
 
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 9b81697..887506d 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1459,6 +1459,13 @@ individually so that features may have different properties
     #
     # Implement pandas methods
     #
+    @doc(pd.DataFrame)
+    def copy(self, deep=True):
+        copied = super().copy(deep=deep)
+        if type(copied) is pd.DataFrame:
+            copied.__class__ = GeoDataFrame
+            copied._geometry_column_name = self._geometry_column_name
+        return copied
 
     def merge(self, *args, **kwargs):
         r"""Merge two ``GeoDataFrame`` objects with a database-style join.

diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index 632db49..73e8463 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -542,6 +542,19 @@ class TestDataFrame:
         assert type(df2) is GeoDataFrame
         assert self.df.crs == df2.crs
 
+    def test_empty_copy(self):
+        # https://github.com/geopandas/geopandas/issues/2765
+        df = GeoDataFrame()
+        df2 = df.copy()
+        assert type(df2) is GeoDataFrame
+        df3 = df.copy(deep=True)
+        assert type(df3) is GeoDataFrame
+
+    def test_no_geom_copy(self):
+        df = GeoDataFrame(pd.DataFrame({"a": [1, 2, 3]}))
+        assert type(df) is GeoDataFrame
+        assert type(df.copy()) is GeoDataFrame
+
     def test_bool_index(self):
         # Find boros with 'B' in their name
         df = self.df[self.df["BoroName"].str.contains("B")]

BUG: copy of empty GeoDataFrame becomes Pandas DataFrame
- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest version of geopandas.

- [x] (optional) I have confirmed this bug exists on the main branch of geopandas.

---


#### Code Sample, a copy-pastable example

```python
import copy
import geopandas as gpd

empty = gpd.GeoDataFrame()
print("original plain:", type(empty))  # GeoDataFrame
print("copied plain:", type(copy.deepcopy(empty)))  # DataFrame !!!!!!!!!!!!!!!!!!

geom = gpd.GeoDataFrame(columns=["geometry"])
print("original with geometry:", type(geom))  # GeoDataFrame
print("copied with geometry:", type(copy.deepcopy(geom)))  # GeoDataFrame
```

#### Problem description

When I [deepcopy](https://docs.python.org/3/library/copy.html) a geopandas.GeoDataFrame without a "geometry" column, the copy becomes a pandas.DataFrame. I looked on the main branches on Github and neither Pandas nor Geopandas override `__deepcopy__`. So I have no idea why this happens.

Stackoverflow users have suggested the workaround of using `GeoDataFrame.copy(deep=True)` but that has three drawbacks:
- It's not a true deep copy. For object columns, individual elements are still passed by reference [instead of being copied](https://stackoverflow.com/questions/66318160/truly-deep-copying-pandas-dataframes).
- It's not applicable when the GeoDataFrame itself is part of a compound object.
- It doesn't work with my versions (see show_versions below).

#### Expected Output

Copying a GeoDataFrame should result in another GeoDataFrame, not a DataFrame.

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.10.8 | packaged by conda-forge | (main, Nov 22 2022, 08:16:33) [MSC v.1929 64 bit (AMD64)]
executable : C:\Users\user\anaconda3\envs\rqa\python.exe
machine    : Windows-10-10.0.19045-SP0

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : 3.11.1
GEOS lib   : None
GDAL       : None
GDAL data dir: None
PROJ       : 9.1.0
PROJ data dir: C:\Users\user\anaconda3\envs\rqa\Library\share\proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.12.2
numpy      : 1.23.5
pandas     : 1.5.2
pyproj     : 3.4.1
shapely    : 2.0.0
fiona      : None
geoalchemy2: None
geopy      : None
matplotlib : 3.6.2
mapclassify: 2.4.3
pygeos     : None
pyogrio    : None
psycopg2   : None
pyarrow    : None
rtree      : 1.0.1

</details>

#### Source

I cross-posted some content from [StackOverflow](https://stackoverflow.com/questions/75173357/deep-copy-of-geodataframe-becomes-pandas-dataframe).
geopandas/tests/test_geodataframe.py::TestDataFrame::test_empty_copy geopandas/tests/test_geodataframe.py::TestDataFrame::test_no_geom_copy
geopandas/tests/test_geodataframe.py::TestDataFrame::test_df_init geopandas/tests/test_geodataframe.py::TestDataFrame::test_different_geo_colname geopandas/tests/test_geodataframe.py::TestDataFrame::test_geo_getitem geopandas/tests/test_geodataframe.py::TestDataFrame::test_getitem_no_geometry geopandas/tests/test_geodataframe.py::TestDataFrame::test_geo_setitem geopandas/tests/test_geodataframe.py::TestDataFrame::test_geometry_property geopandas/tests/test_geodataframe.py::TestDataFrame::test_geometry_property_errors geopandas/tests/test_geodataframe.py::TestDataFrame::test_rename_geometry geopandas/tests/test_geodataframe.py::TestDataFrame::test_set_geometry geopandas/tests/test_geodataframe.py::TestDataFrame::test_set_geometry_col geopandas/tests/test_geodataframe.py::TestDataFrame::test_set_geometry_inplace geopandas/tests/test_geodataframe.py::TestDataFrame::test_set_geometry_series geopandas/tests/test_geodataframe.py::TestDataFrame::test_set_geometry_empty geopandas/tests/test_geodataframe.py::TestDataFrame::test_set_geometry_np_int geopandas/tests/test_geodataframe.py::TestDataFrame::test_get_geometry_invalid geopandas/tests/test_geodataframe.py::TestDataFrame::test_get_geometry_geometry_inactive geopandas/tests/test_geodataframe.py::TestDataFrame::test_align geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_geom_col geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_only_geom_column geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_na geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_bad_na geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_dropna geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_keepna geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_drop_id geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_drop_id_only_geom_column geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_json_with_duplicate_columns geopandas/tests/test_geodataframe.py::TestDataFrame::test_copy geopandas/tests/test_geodataframe.py::TestDataFrame::test_bool_index geopandas/tests/test_geodataframe.py::TestDataFrame::test_coord_slice_points geopandas/tests/test_geodataframe.py::TestDataFrame::test_from_dict geopandas/tests/test_geodataframe.py::TestDataFrame::test_from_features geopandas/tests/test_geodataframe.py::TestDataFrame::test_from_features_unaligned_properties geopandas/tests/test_geodataframe.py::TestDataFrame::test_from_features_empty_properties geopandas/tests/test_geodataframe.py::TestDataFrame::test_from_features_geom_interface_feature geopandas/tests/test_geodataframe.py::TestDataFrame::test_from_feature_collection geopandas/tests/test_geodataframe.py::TestDataFrame::test_dataframe_to_geodataframe geopandas/tests/test_geodataframe.py::TestDataFrame::test_dataframe_not_manipulated geopandas/tests/test_geodataframe.py::TestDataFrame::test_geodataframe_geointerface geopandas/tests/test_geodataframe.py::TestDataFrame::test_geodataframe_iterfeatures geopandas/tests/test_geodataframe.py::TestDataFrame::test_geodataframe_geojson_no_bbox geopandas/tests/test_geodataframe.py::TestDataFrame::test_geodataframe_geojson_bbox geopandas/tests/test_geodataframe.py::TestDataFrame::test_pickle geopandas/tests/test_geodataframe.py::TestDataFrame::test_pickle_method geopandas/tests/test_geodataframe.py::TestDataFrame::test_estimate_utm_crs geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_wkb geopandas/tests/test_geodataframe.py::TestDataFrame::test_to_wkt geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[None-None-left] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[None-None-inner] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[None-None-right] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[None-1-left] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[None-1-inner] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[None-1-right] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[distance-None-left] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[distance-None-inner] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[distance-None-right] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[distance-1-left] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[distance-1-inner] geopandas/tests/test_geodataframe.py::TestDataFrame::test_sjoin_nearest[distance-1-right] geopandas/tests/test_geodataframe.py::TestDataFrame::test_clip geopandas/tests/test_geodataframe.py::TestDataFrame::test_overlay[union] geopandas/tests/test_geodataframe.py::TestDataFrame::test_overlay[intersection] geopandas/tests/test_geodataframe.py::TestDataFrame::test_overlay[difference] geopandas/tests/test_geodataframe.py::TestDataFrame::test_overlay[symmetric_difference] geopandas/tests/test_geodataframe.py::TestDataFrame::test_overlay[identity] geopandas/tests/test_geodataframe.py::TestConstructor::test_dict geopandas/tests/test_geodataframe.py::TestConstructor::test_dict_of_series geopandas/tests/test_geodataframe.py::TestConstructor::test_dict_specified_geometry geopandas/tests/test_geodataframe.py::TestConstructor::test_array geopandas/tests/test_geodataframe.py::TestConstructor::test_from_frame geopandas/tests/test_geodataframe.py::TestConstructor::test_from_frame_specified_geometry geopandas/tests/test_geodataframe.py::TestConstructor::test_only_geometry geopandas/tests/test_geodataframe.py::TestConstructor::test_no_geometries geopandas/tests/test_geodataframe.py::TestConstructor::test_empty geopandas/tests/test_geodataframe.py::TestConstructor::test_column_ordering geopandas/tests/test_geodataframe.py::TestConstructor::test_overwrite_geometry geopandas/tests/test_geodataframe.py::TestConstructor::test_repeat_geo_col geopandas/tests/test_geodataframe.py::TestConstructor::test_multiindex_with_geometry_label[geometry] geopandas/tests/test_geodataframe.py::TestConstructor::test_multiindex_with_geometry_label[object] geopandas/tests/test_geodataframe.py::test_geodataframe_crs